package util

import (
	"Projects/NoteSync/internal/common"
	"bufio"
	"bytes"
	"context"
	"crypto/md5"
	"crypto/sha256"
	"crypto/tls"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"html/template"
	"io"
	"io/ioutil"
	"mime/multipart"
	"net/http"
	"os"
	"runtime"
	"strings"
	"time"

	"github.com/go-basic/uuid"
	"github.com/pkg/errors"
	"github.com/qf-tech/qftech-base/pkg/log"
	qflog "github.com/qf-tech/qftech-base/pkg/log"
	"go.uber.org/zap"

	"github.com/PuerkitoBio/goquery"
	"github.com/antchfx/htmlquery"
	"github.com/russross/blackfriday/v2"
	"golang.org/x/net/html"
)

func CalculateHash(toBeHashed string) string {
	hashInBytes := sha256.Sum256([]byte(toBeHashed))
	hashStr := hex.EncodeToString(hashInBytes[:])
	return hashStr
}

func GenWsse(username string, password string, nonce string) string {
	created := time.Now().Format("2006-01-02T15:04:05Z")
	plainData := fmt.Sprintf("%s%s%s%s", username, password, nonce, created)
	digest := CalculateHash(plainData)

	wsseInfo := fmt.Sprintf("UsernameToken Username=\"%s\", PasswordDigest=\"%s\", Nonce=\"%s\", Created=\"%s\"",
		username, digest, nonce, created)
	return wsseInfo
}

func GenTraceCtx() context.Context {
	tmpSlice := strings.Split(uuid.New(), "-")
	traceID := tmpSlice[len(tmpSlice)-1]
	ctx, _ := log.GetLogger().AddCtx(context.Background(), zap.Any("TraceID", traceID))
	return ctx
}

func HttpPostJson(url string, reqBody interface{}, timeout time.Duration) (interface{}, error) {
	c := &http.Client{Timeout: timeout}

	// adminToken := os.Getenv("ADMIN_TOKEN")
	// req.Header.Add("ADMIN-TOKEN", adminToken)

	bytesData, err := json.Marshal(reqBody)
	if err != nil {
		fmt.Println(err.Error())
		return nil, err
	}
	reader := bytes.NewReader(bytesData)
	req, err := http.NewRequest("POST", url, reader)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/json;charset=UTF-8")

	resp, err := c.Do(req)
	if err != nil {
		return nil, err
	}

	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	var v interface{}
	err = json.Unmarshal(body, &v)
	if err != nil {
		return nil, err
	}

	return v, nil
}

func HttpPostFile(filename string, targetURL string, headers map[string]string, timeout time.Duration) (interface{}, error) {
	qflog.Sugare.Infof("file: %s, url: %s, headers: %v", filename, targetURL, headers)
	bodyBuf := bytes.NewBufferString("")
	bodyWriter := multipart.NewWriter(bodyBuf)

	// use the bodyWriter to write the Part headers to the buffer
	_, err := bodyWriter.CreateFormFile("file", filename)
	if err != nil {
		qflog.Sugare.Error("error writing to buffer")
		return nil, err
	}

	// the file data will be the second part of the body
	fh, err := os.Open(filename)
	if err != nil {
		qflog.Sugare.Error("error opening file")
		return nil, err
	}
	// need to know the boundary to properly close the part myself.
	boundary := bodyWriter.Boundary()
	//close_string := fmt.Sprintf("\r\n--%s--\r\n", boundary)
	closeBuf := bytes.NewBufferString(fmt.Sprintf("\r\n--%s--\r\n", boundary))

	// use multi-reader to defer the reading of the file data until
	// writing to the socket buffer.
	requestReader := io.MultiReader(bodyBuf, fh, closeBuf)
	fi, err := fh.Stat()
	if err != nil {
		qflog.Sugare.Error("Error Stating file: %s", filename)
		return nil, err
	}

	req, err := http.NewRequest("POST", targetURL, requestReader)
	if err != nil {
		qflog.Sugare.Error("new request err: %v", err)
		return nil, err
	}

	// Set headers for multipart, and Content Length
	req.Header.Add("Content-Type", "multipart/form-data; boundary="+boundary)
	for k, v := range headers {
		req.Header.Add(k, v)
	}
	req.ContentLength = fi.Size() + int64(bodyBuf.Len()) + int64(closeBuf.Len())

	c := &http.Client{Timeout: timeout}
	if strings.HasPrefix(targetURL, "https://") {
		c.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
	}
	resp, err := c.Do(req)
	if err != nil {
		qflog.Sugare.Errorf("do request err: %v， fileName: %s", err, filename)
		return nil, err
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		qflog.Sugare.Error("read all from response err: %v", err)
		return nil, err
	}

	var v interface{}
	err = json.Unmarshal(body, &v)
	if err != nil {
		qflog.Sugare.Error("json unmarshal err: %v", err)
		return nil, err
	}

	return v, nil
}

func BaseName(path string, osType string) string {
	tmpPath := strings.Split(path, "?")[0]
	// 如果是windows
	var pos int
	if strings.ToLower(osType) == "windows" || strings.ToLower(runtime.GOOS) == "windows" {
		pos = strings.LastIndex(tmpPath, "\\")
	} else { // linux、darwin(macOS)
		pos = strings.LastIndex(tmpPath, "/")
	}
	return tmpPath[pos+1:]
}

func DownloadFile(url string, dstDir string, filename string) (string, error) {
	var res *http.Response
	var err error

	if strings.HasPrefix(url, "https://") {
		tr := &http.Transport{
			TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} // 若需要测试自签名的证书 这里需设置跳过证书检测
		client := &http.Client{Transport: tr}
		res, err = client.Get(url)
	} else {
		res, err = http.Get(url)
	}
	if err != nil {
		return "", errors.Wrap(err, "get err")
	}
	defer res.Body.Close()

	if _, err := os.Stat(dstDir); os.IsNotExist(err) {
		_ = os.MkdirAll(dstDir, os.ModePerm)
	}

	reader := bufio.NewReaderSize(res.Body, 64*1024)

	// 优先使用filename，若为空，则使用 url 中 name
	if filename == "" {
		filename = BaseName(url, "")
	}
	filePath := dstDir + "/" + filename
	file, err := os.Create(filePath)
	if err != nil {
		return "", errors.Wrap(err, "create err")
	}

	writer := bufio.NewWriter(file)
	if _, err := io.Copy(writer, reader); err != nil {
		return "", errors.Wrap(err, "copy err")
	}

	if res.StatusCode != http.StatusOK {
		tmpMsg, _ := os.ReadFile(filePath)
		errMsg := strings.ReplaceAll(string(tmpMsg), "\n", ", ")
		return "", errors.New(errMsg)
	}

	return filePath, nil
}

// 计算文件的md5
func GetFileMd5(filename string) (string, error) {
	file, err := os.Open(filename)
	if err != nil {
		return "", errors.Wrap(err, "open file err")
	}

	md5 := md5.New()
	if _, err := io.Copy(md5, file); err != nil {
		return "", errors.Wrap(err, "io copy err")
	}

	md5Str := hex.EncodeToString(md5.Sum(nil))
	return md5Str, nil
}

func Trans2Html(filePath string, htmlPath string) error {
	mdContent, err := os.ReadFile(filePath)
	if err != nil {
		return errors.Wrap(err, "open file err")
	}
	htmlOutput := blackfriday.Run(mdContent, blackfriday.WithExtensions(blackfriday.CommonExtensions))

	doc, err := goquery.NewDocumentFromReader(bytes.NewReader(htmlOutput))
	if err != nil {
		return errors.Wrap(err, "read document from reader err")
	}

	// Building the Table of Contents
	toc := buildTOC(doc)
	// doc.PrependHtml(toc)

	doc.Find("table").Each(func(index int, tableHtml *goquery.Selection) {
		tableHtml.AddClass("github-style-table")
	})

	// Load the styles from styles.css
	cssBytes, err := os.ReadFile("styles.css")
	if err != nil {
		return errors.Wrap(err, "open css file err")
	}

	// Create a new head element with the styles
	headContent := fmt.Sprintf("<head><style>%s</style></head>", cssBytes)

	// Check if there's already a head tag and if not, prepend it
	if doc.Find("head").Length() == 0 {
		doc.PrependHtml(headContent)
	} else {
		doc.Find("head").AppendHtml(fmt.Sprintf("<style>%s</style>", cssBytes))
	}

	enhancedHtml, err := doc.Html()
	if err != nil {
		return errors.Wrap(err, "doc to html err")
	}

	ioutil.WriteFile(htmlPath, []byte(enhancedHtml), 0644)

	// 增加目录
	if err := addContentLink(htmlPath, toc); err != nil {
		return errors.Wrap(err, "add conetnt link err")
	}

	return nil
}

func buildTOC(doc *goquery.Document) string {
	var toc strings.Builder
	toc.WriteString("<div id='toc'><b>导航目录</b><ul>")

	doc.Find("body").Find("h1, h2, h3, h4, h5, h6").Each(func(i int, h *goquery.Selection) {
		text := h.Text()
		id := strings.ToLower(strings.ReplaceAll(text, " ", "-"))
		h.SetAttr("id", id)
		toc.WriteString(fmt.Sprintf("<li><a href='#%s'>%s</a></li>", id, text))
	})

	toc.WriteString("</ul></div>")
	return toc.String()
}

func addContentLink(path string, toc string) error {
	doc, err := htmlquery.LoadDoc(path)
	if err != nil {
		return errors.Wrap(err, "html query load src file err")
	}

	node, err := htmlquery.Query(doc, "//head")
	if err != nil {
		return errors.Wrap(err, "html query err")
	}
	node.AppendChild(&html.Node{
		Type: html.RawNode,
		Data: toc,
	})

	file, err := os.OpenFile(path, os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0644)
	if err != nil {
		return errors.Wrap(err, "open file err")
	}
	_ = html.Render(file, doc)

	return nil
}

// MakeBodyFromTemplateV2 从本地的文件系统中加载并渲染指定的邮件模板，
// 模板路径示例：templates/mail/auth/activate。
// 请根据邮件模板目录下的 README 传入对应的 data 参数。
func MakeBodyFromTemplateV2(templatePath string, data map[string]interface{}) ([]byte, error) {
	if !strings.HasSuffix(templatePath, ".html") {
		templatePath += ".html"
	}

	// 预定义的模板变量
	data["year"] = time.Now().Year()

	t, err := template.ParseFiles(templatePath)
	if err != nil {
		return nil, errors.Wrap(err, "parse")
	}

	var tpl bytes.Buffer
	if err := t.Execute(&tpl, data); err != nil {
		return nil, errors.Wrap(err, "execute")
	}
	return tpl.Bytes(), nil
}

func MakeDirExist(dirPath string) error {
	if err := os.MkdirAll(dirPath, os.ModePerm); err != nil {
		return errors.Wrap(err, "mkdir")
	}
	return nil
}

func Trans2HtmlByTmpl(filePath string, htmlPath string, tmplType string) error {
	exist := false
	tmplTypeArr := []common.TmplThemeType{
		common.ThemeTypeNull,
		common.ThemeTypeDefault,
		common.ThemeTypeGithub,
		common.ThemeTypeChrome}
	for _, e := range tmplTypeArr {
		if string(e) == tmplType {
			exist = true
			break
		}
	}
	if !exist {
		return errors.New("tmpleate theme type invalid")
	}
	if tmplType == "" {
		tmplType = string(common.ThemeTypeDefault)
	}

	mdContent, err := os.ReadFile(filePath)
	if err != nil {
		return errors.Wrap(err, "open file err")
	}
	htmlOutput := blackfriday.Run(mdContent, blackfriday.WithExtensions(blackfriday.CommonExtensions))

	gdoc, err := goquery.NewDocumentFromReader(bytes.NewReader(htmlOutput))
	if err != nil {
		return errors.Wrap(err, "read document from reader err")
	}

	// Building the Table of Contents
	toc := buildTOC(gdoc)
	// doc.PrependHtml(toc)

	if tmplType == string(common.ThemeTypeDefault) {
		gdoc.Find("table").Each(func(index int, tableHtml *goquery.Selection) {
			tableHtml.AddClass("github-style-table")
		})
	}
	enhancedHtml, err := gdoc.Html()
	if err != nil {
		return errors.Wrap(err, "gdoc to html err")
	}

	body, err := MakeBodyFromTemplateV2(
		fmt.Sprintf("../assets/templates/themes/%s.html", tmplType),
		map[string]interface{}{
			"CONTENT_LIST": "",
			"CONTENT":      "",
		},
	)
	if err != nil {
		return errors.Wrap(err, "make body err")
	}

	os.WriteFile(htmlPath, body, 0644)

	doc, err := htmlquery.LoadDoc(htmlPath)
	if err != nil {
		return errors.Wrap(err, "html query load src file err")
	}

	node, err := htmlquery.Query(doc, "//body/article")
	if err != nil {
		return errors.Wrap(err, "html query err")
	}

	node.AppendChild(&html.Node{
		Type: html.RawNode,
		Data: toc,
	})

	node.AppendChild(&html.Node{
		Type: html.RawNode,
		Data: enhancedHtml,
	})

	file, err := os.OpenFile(htmlPath, os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0644)
	if err != nil {
		return errors.Wrap(err, "open file err")
	}
	_ = html.Render(file, doc)

	return nil
}
